الگوهای پیشرفته شیءگرایی تایپاسکریپت را بررسی کنید. این راهنما اصول طراحی کلاس، بحث وراثت در مقابل ترکیب، و استراتژیهای عملی برای ساخت برنامههای مقیاسپذیر و قابل نگهداری برای مخاطبان جهانی را پوشش میدهد.
الگوهای شیءگرایی تایپاسکریپت: راهنمایی برای طراحی کلاس و استراتژیهای وراثت
در دنیای توسعه نرمافزار مدرن، تایپاسکریپت به عنوان سنگ بنای ساخت برنامههای قوی، مقیاسپذیر و قابل نگهداری ظهور کرده است. سیستم نوعدهی قوی آن که بر روی جاوااسکریپت ساخته شده است، ابزارهایی را در اختیار توسعهدهندگان قرار میدهد تا خطاها را زودتر شناسایی کرده و کد قابل پیشبینیتری بنویسند. در قلب قدرت تایپاسکریپت، پشتیبانی جامع آن از اصول برنامهنویسی شیءگرا (OOP) نهفته است. با این حال، صرفاً دانستن نحوه ایجاد یک کلاس کافی نیست. تسلط بر تایپاسکریپت مستلزم درک عمیق از طراحی کلاس، سلسله مراتب وراثت و سازش بین الگوهای مختلف معماری است.
این راهنما برای مخاطبان جهانی توسعهدهندگان، از کسانی که مهارتهای متوسط خود را تثبیت میکنند تا معماران باتجربه، طراحی شده است. ما عمیقاً در مفاهیم اصلی OOP در تایپاسکریپت غوطه ور خواهیم شد، استراتژیهای مؤثر طراحی کلاس را بررسی میکنیم و به بحث قدیمی میپردازیم: وراثت در مقابل ترکیب. در پایان، شما به دانشی مجهز خواهید شد تا تصمیمات طراحی آگاهانهای بگیرید که منجر به کدهای تمیزتر، انعطافپذیرتر و مقاومتر در برابر آینده شود.
درک ارکان OOP در تایپاسکریپت
قبل از اینکه به الگوهای پیچیده بپردازیم، بیایید با بازبینی چهار رکن اساسی برنامهنویسی شیءگرا، همانطور که در تایپاسکریپت اعمال میشوند، پایهای محکم ایجاد کنیم.
1. کپسولهسازی
کپسولهسازی اصل بستهبندی دادههای یک شیء (ویژگیها) و روشهایی است که بر روی آن دادهها عمل میکنند در یک واحد منفرد - یک کلاس. همچنین شامل محدود کردن دسترسی مستقیم به وضعیت داخلی یک شیء است. تایپاسکریپت این کار را عمدتاً از طریق اصلاحکنندههای دسترسی انجام میدهد: public، private و protected.
مثال: یک حساب بانکی که موجودی فقط از طریق روشهای واریز و برداشت قابل تغییر است.
class BankAccount {
private balance: number = 0;
constructor(initialBalance: number) {
if (initialBalance >= 0) {
this.balance = initialBalance;
}
}
public deposit(amount: number): void {
if (amount > 0) {
this.balance += amount;
console.log(`Deposited: ${amount}. New balance: ${this.balance}`);
}
}
public getBalance(): number {
// We expose the balance through a method, not directly
return this.balance;
}
}
2. انتزاع
انتزاع به معنای پنهان کردن جزئیات پیچیده پیادهسازی و افشای تنها ویژگیهای ضروری یک شیء است. این به ما امکان میدهد بدون نیاز به درک ماشین آلات پیچیده زیربنایی، با مفاهیم سطح بالا کار کنیم. در تایپاسکریپت، انتزاع اغلب با استفاده از کلاسهای abstract و interfaces به دست میآید.
مثال: وقتی از ریموت کنترل استفاده میکنید، فقط دکمه "Power" را فشار میدهید. نیازی نیست در مورد سیگنالهای مادون قرمز یا مدار داخلی بدانید. ریموت یک رابط انتزاعی برای عملکرد تلویزیون ارائه میدهد.
3. وراثت
وراثت مکانیزمی است که در آن یک کلاس جدید (زیرکلاس یا کلاس مشتق شده) ویژگیها و روشها را از یک کلاس موجود (ابرکلاس یا کلاس پایه) به ارث میبرد. این امر استفاده مجدد از کد را ترویج میکند و یک رابطه واضح "is-a" بین کلاسها ایجاد میکند. تایپاسکریپت از کلمه کلیدی extends برای وراثت استفاده میکند.
مثال: یک `Manager` نوعی `Employee` "is-a" است. آنها ویژگیهای مشترکی مانند `name` و `id` دارند، اما `Manager` ممکن است ویژگیهای اضافی مانند `subordinates` داشته باشد.
class Employee {
constructor(public name: string, public id: number) {}
getProfile(): string {
return `Name: ${this.name}, ID: ${this.id}`;
}
}
class Manager extends Employee {
constructor(name: string, id: number, public subordinates: Employee[]) {
super(name, id); // Call the parent constructor
}
// Managers can also have their own methods
delegateTask(): void {
console.log(`${this.name} is delegating tasks.`);
}
}
4. چندریختی
چندریختی، که به معنای "اشکال مختلف" است، به اشیاء کلاسهای مختلف اجازه میدهد تا به عنوان اشیاء یک ابرکلاس مشترک رفتار شوند. این امر یک رابط واحد (مانند نام روش) را قادر میسازد تا اشکال مختلف زیربنایی (پیادهسازیها) را نشان دهد. این اغلب از طریق بازنویسی روش به دست میآید.
مثال: یک متد `render()` که برای یک شیء `Circle` در مقابل یک شیء `Square` متفاوت رفتار میکند، حتی اگر هر دو `Shape` باشند.
abstract class Shape {
abstract draw(): void; // An abstract method must be implemented by subclasses
}
class Circle extends Shape {
draw(): void {
console.log("Drawing a circle.");
}
}
class Square extends Shape {
draw(): void {
console.log("Drawing a square.");
}
}
function renderShapes(shapes: Shape[]): void {
shapes.forEach(shape => shape.draw()); // Polymorphism in action!
}
const myShapes: Shape[] = [new Circle(), new Square()];
renderShapes(myShapes);
// Output:
// Drawing a circle.
// Drawing a square.
بحث بزرگ: وراثت در مقابل ترکیب
این یکی از مهمترین تصمیمات طراحی در OOP است. خرد رایج در مهندسی نرمافزار مدرن این است که "ترکیب را به وراثت ترجیح دهید." بیایید با بررسی عمیق هر دو مفهوم، دلیل آن را درک کنیم.
وراثت چیست؟ رابطه "is-a"
وراثت یک جفتشدگی تنگاتنگ بین کلاس پایه و کلاس مشتق شده ایجاد میکند. وقتی از `extends` استفاده میکنید، بیان میکنید که کلاس جدید یک نسخه تخصصی از کلاس پایه است. این یک ابزار قدرتمند برای استفاده مجدد از کد است زمانی که یک رابطه سلسله مراتبی واضح وجود داشته باشد.
- مزایا:
- استفاده مجدد از کد: منطق مشترک یک بار در کلاس پایه تعریف میشود.
- چندریختی: امکان رفتار چندریختی ظریف را فراهم میکند، همانطور که در مثال `Shape` ما دیده شد.
- سلسله مراتب واضح: یک سیستم طبقهبندی بالا به پایین واقعی را مدلسازی میکند.
- معایب:
- جفتشدگی تنگاتنگ: تغییرات در کلاس پایه میتواند به طور ناخواسته کلاسهای مشتق شده را خراب کند. این به عنوان "مشکل کلاس پایه شکننده" شناخته میشود.
- جهنم سلسله مراتب: استفاده بیش از حد میتواند منجر به زنجیرههای وراثت عمیق، پیچیده و سخت شود که درک و نگهداری آنها دشوار است.
- انعطاف ناپذیر: یک کلاس فقط میتواند از یک کلاس دیگر در تایپاسکریپت (وراثت تکی) به ارث ببرد، که میتواند محدود کننده باشد. شما نمیتوانید ویژگیها را از چندین کلاس غیر مرتبط به ارث ببرید.
چه زمانی وراثت یک انتخاب خوب است؟
زمانی از وراثت استفاده کنید که رابطه واقعاً "is-a" باشد و پایدار باشد و بعید است تغییر کند. به عنوان مثال، `CheckingAccount` و `SavingsAccount` هر دو اساساً انواع `BankAccount` هستند. این سلسله مراتب منطقی است و بعید است که دوباره مدلسازی شود.
ترکیب چیست؟ رابطه "has-a"
ترکیب شامل ساخت اشیاء پیچیده از اشیاء کوچکتر و مستقل است. به جای اینکه یک کلاس چیزی باشد، دارای اشیاء دیگری است که عملکرد مورد نیاز را ارائه میدهند. این یک جفتشدگی سست ایجاد میکند، زیرا کلاس فقط با رابط عمومی اشیاء ترکیب شده تعامل دارد.
- مزایا:
- انعطاف پذیری: با تعویض اشیاء ترکیب شده، میتوان عملکرد را در زمان اجرا تغییر داد.
- جفتشدگی سست: کلاس حاوی نیازی به دانستن عملکرد داخلی اجزای مورد استفاده خود ندارد. این امر آزمایش و نگهداری کد را آسانتر میکند.
- اجتناب از مسائل سلسله مراتبی: میتوانید عملکردهای مختلف را از منابع مختلف بدون ایجاد یک درخت وراثت درهم تنیده ترکیب کنید.
- مسئولیتهای واضح: هر کلاس جزء میتواند به اصل مسئولیت واحد پایبند باشد.
- معایب:
- کد دیگ بخار بیشتر: گاهی اوقات در مقایسه با یک مدل وراثت ساده، به کد بیشتری برای سیمکشی اجزای مختلف نیاز دارد.
- کمتر شهودی برای سلسله مراتب: تاکسونومیهای طبیعی را به طور مستقیم مانند وراثت مدلسازی نمیکند.
یک مثال عملی: خودرو
یک `Car` یک مثال عالی از ترکیب است. یک `Car` نوعی `Engine` نیست، و نه نوعی `Wheel` است. در عوض، یک `Car` دارای یک `Engine` و دارای `Wheels` است.
// Component classes
class Engine {
start() {
console.log("Engine starting...");
}
}
class GPS {
navigate(destination: string) {
console.log(`Navigating to ${destination}...`);
}
}
// The composite class
class Car {
private readonly engine: Engine;
private readonly gps: GPS;
constructor() {
// The Car creates its own parts
this.engine = new Engine();
this.gps = new GPS();
}
driveTo(destination: string) {
this.engine.start();
this.gps.navigate(destination);
console.log("Car is on its way.");
}
}
const myCar = new Car();
myCar.driveTo("New York City");
این طراحی بسیار انعطافپذیر است. اگر میخواهیم یک `Car` با یک `ElectricEngine` ایجاد کنیم، به یک زنجیره وراثت جدید نیاز نداریم. میتوانیم از تزریق وابستگی برای تهیه `Car` با اجزای آن استفاده کنیم و آن را حتی ماژولارتر کنیم.
interface IEngine {
start(): void;
}
class PetrolEngine implements IEngine {
start() { console.log("Petrol engine starting..."); }
}
class ElectricEngine implements IEngine {
start() { console.log("Silent electric engine starting..."); }
}
class AdvancedCar {
// The car depends on an abstraction (interface), not a concrete class
constructor(private engine: IEngine) {}
startJourney() {
this.engine.start();
console.log("Journey has begun.");
}
}
const tesla = new AdvancedCar(new ElectricEngine());
tesla.startJourney();
const ford = new AdvancedCar(new PetrolEngine());
ford.startJourney();
استراتژیها و الگوهای پیشرفته در تایپاسکریپت
فراتر از انتخاب اساسی بین وراثت و ترکیب، تایپاسکریپت ابزارهای قدرتمندی را برای ایجاد طرحهای کلاس پیچیده و انعطافپذیر ارائه میدهد.
1. کلاسهای انتزاعی: طرح کلی برای وراثت
وقتی رابطه "is-a" قوی دارید اما میخواهید اطمینان حاصل کنید که کلاسهای پایه نمیتوانند به تنهایی نمونهسازی شوند، از کلاسهای `abstract` استفاده کنید. آنها به عنوان یک طرح کلی عمل میکنند، روشها و ویژگیهای مشترک را تعریف میکنند و میتوانند روشهای `abstract` را اعلام کنند که کلاسهای مشتق شده باید پیادهسازی کنند.
مورد استفاده: یک سیستم پردازش پرداخت. شما میدانید که هر درگاه باید متدهای `pay()` و `refund()` داشته باشد، اما پیادهسازی برای هر ارائه دهنده خاص است (به عنوان مثال، Stripe، PayPal).
abstract class PaymentGateway {
constructor(public apiKey: string) {}
// A concrete method shared by all subclasses
protected connect(): void {
console.log("Connecting to payment service...");
}
// Abstract methods that subclasses must implement
abstract processPayment(amount: number): boolean;
abstract issueRefund(transactionId: string): boolean;
}
class StripeGateway extends PaymentGateway {
processPayment(amount: number): boolean {
this.connect();
console.log(`Processing ${amount} via Stripe.`);
return true;
}
issueRefund(transactionId: string): boolean {
console.log(`Refunding transaction ${transactionId} via Stripe.`);
return true;
}
}
// const gateway = new PaymentGateway("key"); // Error: Cannot create an instance of an abstract class.
const stripe = new StripeGateway("sk_test_123");
stripe.processPayment(100);
2. رابطها: تعریف قراردادها برای رفتار
رابطها در تایپاسکریپت راهی برای تعریف قرارداد برای شکل یک کلاس هستند. آنها مشخص میکنند که یک کلاس باید چه ویژگیها و روشهایی داشته باشد، اما هیچ پیادهسازی ارائه نمیدهند. یک کلاس میتواند چندین رابط را `implement` کند و آنها را به سنگ بنای طراحی ترکیبی و جدا شده تبدیل میکند.
رابط در مقابل کلاس انتزاعی
- از یک کلاس انتزاعی استفاده کنید زمانی که میخواهید کد پیادهسازی شده را بین چندین کلاس مرتبط از نزدیک به اشتراک بگذارید.
- از یک رابط استفاده کنید زمانی که میخواهید یک قرارداد برای رفتاری تعریف کنید که میتواند توسط کلاسهای ناهمگون و غیر مرتبط پیادهسازی شود.
مورد استفاده: در یک سیستم، ممکن است اشیاء مختلفی نیاز به سریالسازی به یک قالب رشته داشته باشند (به عنوان مثال، برای ثبت یا ذخیرهسازی). این اشیاء (`User`، `Product`، `Order`) غیر مرتبط هستند اما یک قابلیت مشترک دارند.
interface ISerializable {
serialize(): string;
}
class User implements ISerializable {
constructor(public id: number, public name: string) {}
serialize(): string {
return JSON.stringify({ id: this.id, name: this.name });
}
}
class Product implements ISerializable {
constructor(public sku: string, public price: number) {}
serialize(): string {
return JSON.stringify({ sku: this.sku, price: this.price });
}
}
function logItems(items: ISerializable[]): void {
items.forEach(item => {
console.log("Serialized item:", item.serialize());
});
}
const user = new User(1, "Alice");
const product = new Product("TSHIRT-RED", 19.99);
logItems([user, product]);
3. میکساینها: یک رویکرد ترکیبی برای استفاده مجدد از کد
از آنجایی که تایپاسکریپت فقط اجازه وراثت تکی را میدهد، اگر بخواهید از کد چندین منبع استفاده مجدد کنید چه؟ اینجاست که الگوی میکساین وارد میشود. میکساینها توابعی هستند که یک سازنده را میگیرند و یک سازنده جدید را برمیگردانند که آن را با عملکرد جدید گسترش میدهد. این شکلی از ترکیب است که به شما امکان میدهد قابلیتها را در یک کلاس "مخلوط کنید".
مورد استفاده: شما میخواهید رفتارهای `Timestamp` (با `createdAt`، `updatedAt`) و `SoftDeletable` (با یک ویژگی `deletedAt` و متد `softDelete()`) را به چندین کلاس مدل اضافه کنید.
// A Type helper for mixins
type Constructor = new (...args: any[]) => T;
// Timestamp Mixin
function Timestamped(Base: TBase) {
return class extends Base {
createdAt: Date = new Date();
updatedAt: Date = new Date();
};
}
// SoftDeletable Mixin
function SoftDeletable(Base: TBase) {
return class extends Base {
deletedAt: Date | null = null;
softDelete() {
this.deletedAt = new Date();
console.log("Item has been soft deleted.");
}
};
}
// Base class
class DocumentModel {
constructor(public title: string) {}
}
// Create a new class by composing mixins
const UserAccountModel = SoftDeletable(Timestamped(DocumentModel));
const userAccount = new UserAccountModel("My User Account");
console.log(userAccount.title);
console.log(userAccount.createdAt);
userAccount.softDelete();
console.log(userAccount.deletedAt);
نتیجه گیری: ساخت برنامههای تایپاسکریپت مقاوم در برابر آینده
تسلط بر برنامهنویسی شیءگرا در تایپاسکریپت سفری است از درک نحو تا پذیرش فلسفه طراحی. انتخابهایی که در مورد ساختار کلاس، وراثت و ترکیب انجام میدهید، تأثیر عمیقی بر سلامت بلندمدت برنامه شما دارد.
در اینجا نکات کلیدی برای تمرین توسعه جهانی شما آورده شده است:
- با ارکان شروع کنید: اطمینان حاصل کنید که درک محکمی از کپسولهسازی، انتزاع، وراثت و چندریختی دارید. آنها واژگان OOP هستند.
- ترکیب را به وراثت ترجیح دهید: این اصل شما را به کد انعطافپذیرتر، ماژولارتر و قابل آزمایشتر سوق میدهد. با ترکیب شروع کنید و فقط زمانی به وراثت دست یابید که یک رابطه "is-a" واضح و پایدار وجود داشته باشد.
- از ابزار مناسب برای کار استفاده کنید:
- از وراثت برای تخصص واقعی و اشتراکگذاری کد در یک سلسله مراتب پایدار استفاده کنید.
- از کلاسهای انتزاعی برای تعریف یک پایه مشترک برای خانوادهای از کلاسها استفاده کنید، برخی از پیادهسازیها را به اشتراک بگذارید در حالی که یک قرارداد را اعمال میکنید.
- از رابطها برای تعریف قراردادهایی برای رفتاری استفاده کنید که میتواند توسط هر کلاسی پیادهسازی شود و جداسازی شدید را ترویج کند.
- از میکساینها استفاده کنید زمانی که نیاز دارید عملکردها را از چندین منبع در یک کلاس ترکیب کنید و بر محدودیتهای وراثت تکی غلبه کنید.
با تفکر انتقادی در مورد این الگوها و درک سازشهای آنها، میتوانید برنامههای تایپاسکریپت را طراحی کنید که نه تنها امروزه قدرتمند و کارآمد هستند، بلکه به راحتی قابل انطباق، گسترش و نگهداری برای سالهای آینده هستند - مهم نیست شما یا تیمتان در کجای دنیا باشید.